package com.javabi.sizeof.definition;

import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;

import com.javabi.sizeof.ClassDefinition;
import com.javabi.sizeof.ClassDefinitionMap;
import com.javabi.sizeof.MemoryUtil;

/**
 * A Hash Map Definition (Safe).
 * <p>
 * This definition is 'safe' because it adds the Entry objects to the instance list. It also provides a minor performance enhancement on the default calculation strategy.
 * </p>
 * @see HashMap
 * @see HashSet
 */
@SuppressWarnings("unchecked")
public class HashMapDefinition extends ClassDefinition<HashMap> {

	/** The HashMap.table field. */
	private final Field tableField;
	/** The HashMap$Entry class. */
	private final Class<?> entryClass;
	/** The Entry.key field. */
	private final Field keyField;
	/** The Entry.value field. */
	private final Field valueField;
	/** The Entry.next field. */
	private final Field nextField;

	/**
	 * Creates a linked list definition.
	 */
	public HashMapDefinition() throws ClassNotFoundException, NoSuchFieldException {
		tableField = HashMap.class.getDeclaredField("table");
		tableField.setAccessible(true);
		entryClass = Class.forName("java.util.HashMap$Entry");
		keyField = entryClass.getDeclaredField("key");
		keyField.setAccessible(true);
		valueField = entryClass.getDeclaredField("value");
		valueField.setAccessible(true);
		nextField = entryClass.getDeclaredField("next");
		nextField.setAccessible(true);
	}

	@Override
	public long sizeOf(HashMap map) {
		return 36; // HashMap instance base size
	}

	@Override
	public final boolean hasElements(HashMap map) {
		return true;
	}

	@Override
	public long sizeOfElements(HashMap map, ClassDefinitionMap definitionMap, Set<Object> instanceSet, long size) {
		int mapSize = map.size();
        Object[] table = new Object[0];
        try {
            table = (Object[]) tableField.get(map);
        } catch (IllegalAccessException e) {

        }
        instanceSet.add(table);
		size += sizeOfIntArray(table.length);

		// Iterate over entries
		if (mapSize > 0) {
			for (Object entry : table) {
				while (entry != null) {
					instanceSet.add(entry);
					size += 24; // Entry instance base size

                    Object key = null;
                    try {
                        key = keyField.get(entry);
                    } catch (IllegalAccessException e) {

                    }
                    Object value = null;
                    try {
                        value = valueField.get(entry);
                    } catch (IllegalAccessException e) {

                    }
                    if (key != null) {
						size += MemoryUtil.sizeOf(key, definitionMap, instanceSet);
					}
					if (key != value && value != null) {
						size += MemoryUtil.sizeOf(value, definitionMap, instanceSet);
					}

					// Short circuit - helps when small or poorly distributed
					mapSize--;
					if (mapSize == 0) {
						break;
					}

					// Next?
                    try {
                        entry = nextField.get(entry);
                    } catch (IllegalAccessException e) {

                    }
                }
			}
		}
		return size;
	}

}
